Micron Document
--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
| SparkN0de-git | SparkN0de |
--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------


Commit 8522a5fd9eee9dd0e2dd6a587fa55fe6a624a9ae


Parents : 112dc16
Author : Ivan <e46112d44649266d71fe2193e00a4710>
Signature : T66BB85Valid, signed by author
Date : 2026-07-19T06:21:15-05:00

feat: fix WebGL label handling with LOD-based visibility and truncation for long names

Changes
Diff

diff --git a/meshchatx.rsm b/meshchatx.rsm
index eb613e2a..3308587f 100644
Binary files a/meshchatx.rsm and b/meshchatx.rsm differ

diff --git a/meshchatx/src/frontend/js/networkVisualiserWebGL.js b/meshchatx/src/frontend/js/networkVisualiserWebGL.js
index bad255be..7753a92a 100644
--- a/meshchatx/src/frontend/js/networkVisualiserWebGL.js
+++ b/meshchatx/src/frontend/js/networkVisualiserWebGL.js
@@ -633,7 +633,7 @@ export function createNetworkVisualiserWebGL(canvas, gl) {
* @param {Float32Array} edges packed EDGE_STRIDE
* @param {{x:number,y:number,zoom:number}} camera
* @param {boolean} dark
- * @param {{x:number,y:number,size:number,text:string}[]} [labels]
+ * @param {{x:number,y:number,size:number,text:string,fontSize?:number}[]} [labels]
*/
function draw(nodes, edges, camera, dark, labels) {
const size = resize();
@@ -716,18 +716,17 @@ export function createNetworkVisualiserWebGL(canvas, gl) {
if (labelCtx && labelCanvas) {
labelCtx.setTransform(1, 0, 0, 1, 0, 0);
labelCtx.clearRect(0, 0, labelCanvas.width, labelCanvas.height);
- if (zoom >= 0.45 && Array.isArray(labels) && labels.length > 0) {
- // Draw in device pixels so glyph AA matches the HiDPI canvas.
- const fontPx = Math.max(12, Math.round(12 * dpr));
+ // LOD (when labels appear) is decided by the engine via collectWebGLLabels.
+ if (Array.isArray(labels) && labels.length > 0) {
labelCtx.textAlign = "center";
labelCtx.textBaseline = "top";
- labelCtx.font = `500 ${fontPx}px Inter, system-ui, -apple-system, "Segoe UI", sans-serif`;
labelCtx.imageSmoothingEnabled = true;
const fill = dark ? "#f4f4f5" : "#18181b";
const stroke = dark ? "rgba(9,9,11,0.75)" : "rgba(255,255,255,0.88)";
labelCtx.lineJoin = "round";
labelCtx.miterLimit = 2;
labelCtx.lineWidth = Math.max(2, Math.round(2.25 * dpr));
+ let lastFontKey = "";
for (const lab of labels) {
if (!lab?.text) continue;
const sx = (lab.x - camX) * zoom + cssW * 0.5;
@@ -737,6 +736,13 @@ export function createNetworkVisualiserWebGL(canvas, gl) {
// Snap to device pixels to avoid blurry half-pixel text.
const tx = Math.round(sx * dpr);
const ty = Math.round((sy + r + 4) * dpr);
+ const cssFont = Math.max(11, Number(lab.fontSize) || 11);
+ const fontPx = Math.max(11, Math.round(cssFont * dpr));
+ const fontKey = String(fontPx);
+ if (fontKey !== lastFontKey) {
+ labelCtx.font = `500 ${fontPx}px Inter, system-ui, -apple-system, "Segoe UI", sans-serif`;
+ lastFontKey = fontKey;
+ }
labelCtx.strokeStyle = stroke;
labelCtx.fillStyle = fill;
labelCtx.strokeText(lab.text, tx, ty);

diff --git a/meshchatx/src/frontend/js/networkVisualiserWebGLEngine.js b/meshchatx/src/frontend/js/networkVisualiserWebGLEngine.js
index 4c377825..ee5c5a54 100644
--- a/meshchatx/src/frontend/js/networkVisualiserWebGLEngine.js
+++ b/meshchatx/src/frontend/js/networkVisualiserWebGLEngine.js
@@ -4,6 +4,7 @@
*/
import { callVisualiserWasmJson, isVisualiserWebGLSceneReady } from "./VisualiserWasmLoader.js";
+import { lodLevelFromScale } from "./networkVisualiserPerf.js";
import {
createNetworkVisualiserWebGL,
mergeSceneNodesWithTextures,
@@ -20,6 +21,70 @@ export const KIND_IFACE_OFF = 2;
export const KIND_PEER = 3;
export const KIND_DISCOVERED = 4;
+/** Max characters drawn on a WebGL node label before ellipsis. */
+export const WEBGL_LABEL_MAX_CHARS = 28;
+
+/**
+ * Truncate a node label for the WebGL overlay.
+ * @param {string|null|undefined} text
+ * @param {number} [maxChars]
+ * @returns {string|null}
+ */
+export function truncateWebGLLabel(text, maxChars = WEBGL_LABEL_MAX_CHARS) {
+ if (typeof text !== "string" || !text) return null;
+ const limit = Number.isFinite(maxChars) && maxChars > 1 ? Math.floor(maxChars) : WEBGL_LABEL_MAX_CHARS;
+ if (text.length <= limit) return text;
+ return `${text.slice(0, Math.max(1, limit - 3))}...`;
+}
+
+/**
+ * Build overlay labels using the same LOD bands as the vis-network canvas path.
+ * low: none, medium: me + hover only, high: all in-scene labels.
+ *
+ * @param {{
+ * zoom: number,
+ * sceneCount: number,
+ * nodes: Float32Array|number[]|null|undefined,
+ * labelByIndex: (string|null|undefined)[],
+ * idByIndex: (string|null|undefined)[],
+ * hoverId?: string|null,
+ * }} opts
+ * @returns {{x:number,y:number,size:number,text:string,fontSize:number}[]}
+ */
+export function collectWebGLLabels(opts) {
+ const zoom = opts?.zoom > 0 ? opts.zoom : 1;
+ const lod = lodLevelFromScale(zoom);
+ if (lod === "low") return [];
+
+ const sceneCount = Math.max(0, opts?.sceneCount | 0);
+ const nodes = opts?.nodes;
+ const labelByIndex = opts?.labelByIndex || [];
+ const idByIndex = opts?.idByIndex || [];
+ const hoverId = opts?.hoverId ? String(opts.hoverId) : null;
+ const out = [];
+
+ for (let i = 0; i < sceneCount; i++) {
+ const id = idByIndex[i] != null ? String(idByIndex[i]) : null;
+ const isMe = id === "me";
+ const isHover = hoverId != null && id === hoverId;
+ if (lod === "medium" && !isMe && !isHover) continue;
+
+ const raw = labelByIndex[i];
+ const text = truncateWebGLLabel(raw);
+ if (!text) continue;
+
+ const o = i * SCENE_NODE_STRIDE;
+ out.push({
+ x: nodes?.[o] ?? 0,
+ y: nodes?.[o + 1] ?? 0,
+ size: nodes?.[o + 2] ?? 10,
+ text,
+ fontSize: isMe ? 16 : 11,
+ });
+ }
+ return out;
+}
+
const DEFAULT_ICON_BY_KIND = {
[KIND_ME]: "/assets/images/reticulum_logo_512.png",
[KIND_IFACE_ON]: "/assets/images/network-visualiser/interface_connected.png",
@@ -220,6 +285,8 @@ export function createVisualiserWebGLEngine(canvas, hooks = {}) {
let imageByIndex = [];
/** @type {(string|null)[]} */
let labelByIndex = [];
+ /** @type {(string|null)[]} */
+ let idByIndex = [];
/** @type {{useTex:number,u:number,v:number}[]} */
let texMeta = [];
let drawNodeScratch = new Float32Array(0);
@@ -231,6 +298,8 @@ export function createVisualiserWebGLEngine(canvas, hooks = {}) {
let lastY = 0;
let nodeCount = 0;
let edgeCount = 0;
+ /** @type {string|null} */
+ let hoverId = null;
/** @type {Map<number,{x:number,y:number}>} */
const pointers = new Map();
let pinchLastDist = 0;
@@ -282,6 +351,8 @@ export function createVisualiserWebGLEngine(canvas, hooks = {}) {
indexById.clear();
imageByIndex = [];
labelByIndex = [];
+ idByIndex = [];
+ hoverId = null;
let idx = 0;
for (const n of graphNodes || []) {
if (!n?.id) continue;
@@ -295,6 +366,7 @@ export function createVisualiserWebGLEngine(canvas, hooks = {}) {
announce: n._announce || null,
});
indexById.set(id, idx);
+ idByIndex[idx] = id;
imageByIndex[idx] = imageForNode(n, kind);
labelByIndex[idx] = typeof n.label === "string" && n.label ? n.label : null;
idx += 1;
@@ -390,20 +462,14 @@ export function createVisualiserWebGLEngine(canvas, hooks = {}) {
drawNodeScratch = new Float32Array(need);
}
const drawNodes = mergeSceneNodesWithTextures(buf.nodes, texMeta, drawNodeScratch);
- const labels = [];
- if (camera.zoom >= 0.45) {
- for (let i = 0; i < sceneCount; i++) {
- const text = labelByIndex[i];
- if (!text) continue;
- const o = i * SCENE_NODE_STRIDE;
- labels.push({
- x: buf.nodes[o],
- y: buf.nodes[o + 1],
- size: buf.nodes[o + 2],
- text,
- });
- }
- }
+ const labels = collectWebGLLabels({
+ zoom: camera.zoom,
+ sceneCount,
+ nodes: buf.nodes,
+ labelByIndex,
+ idByIndex,
+ hoverId,
+ });
const size = renderer.draw(drawNodes, buf.edges, camera, dark, labels);
callScene("meshchatxVisualiserSceneResize", size.width, size.height);
nodeCount = buf.nodeCount || nodeCount;
@@ -475,9 +541,15 @@ export function createVisualiserWebGLEngine(canvas, hooks = {}) {
lastX = p.x;
lastY = p.y;
dirty = true;
- } else if (typeof hooks.onHover === "function") {
+ } else {
const id = callScene("meshchatxVisualiserScenePick", p.x, p.y, 14) || null;
- hooks.onHover(id, id ? metaById.get(id) || null : null, p.x, p.y);
+ if (id !== hoverId) {
+ hoverId = id;
+ dirty = true;
+ }
+ if (typeof hooks.onHover === "function") {
+ hooks.onHover(id, id ? metaById.get(id) || null : null, p.x, p.y);
+ }
}
}
@@ -564,6 +636,8 @@ export function createVisualiserWebGLEngine(canvas, hooks = {}) {
indexById.clear();
imageByIndex = [];
labelByIndex = [];
+ idByIndex = [];
+ hoverId = null;
texMeta = [];
}

diff --git a/tests/frontend/behaviorContracts.test.js b/tests/frontend/behaviorContracts.test.js
index c403a734..df9d1ff2 100644
--- a/tests/frontend/behaviorContracts.test.js
+++ b/tests/frontend/behaviorContracts.test.js
@@ -292,11 +292,14 @@ describe("behavior contracts: network visualiser performance", () => {
expect(engine).toContain("meshchatxVisualiserSceneZoomAt");
expect(engine).toContain("updateNodeImages");
expect(engine).toContain("labelByIndex");
+ expect(engine).toContain("collectWebGLLabels");
+ expect(engine).toContain("lodLevelFromScale");
const webgl = readSource("meshchatx/src/frontend/js/networkVisualiserWebGL.js");
expect(webgl).toContain("u_atlas");
expect(webgl).toContain("network-webgl-labels");
expect(webgl).toContain("resolveVisualiserAssetUrl");
expect(webgl).toContain("mergeSceneNodesWithTextures");
+ expect(webgl).not.toContain("zoom >= 0.45");
const prefs = readSource("meshchatx/src/frontend/js/settings/settingsVisualiserPrefs.js");
expect(prefs).toContain("persistVisualiserRenderer");
expect(prefs).toContain('"auto"');

diff --git a/tests/frontend/networkVisualiserWebGLEngine.test.js b/tests/frontend/networkVisualiserWebGLEngine.test.js
index 3843af05..0c850b0f 100644
--- a/tests/frontend/networkVisualiserWebGLEngine.test.js
+++ b/tests/frontend/networkVisualiserWebGLEngine.test.js
@@ -11,6 +11,9 @@ import {
pointerMidpoint,
canUseVisualiserWebGL,
createVisualiserWebGLEngine,
+ collectWebGLLabels,
+ truncateWebGLLabel,
+ WEBGL_LABEL_MAX_CHARS,
} from "@/js/networkVisualiserWebGLEngine.js";
import {
atlasUvForSlot,
@@ -185,6 +188,37 @@ describe("networkVisualiserWebGLEngine", () => {
expect(canUseVisualiserWebGL()).toBe(false);
});
+ it("truncateWebGLLabel caps long names", () => {
+ expect(truncateWebGLLabel("")).toBeNull();
+ expect(truncateWebGLLabel(null)).toBeNull();
+ expect(truncateWebGLLabel("short")).toBe("short");
+ const long = "a".repeat(WEBGL_LABEL_MAX_CHARS + 10);
+ const out = truncateWebGLLabel(long);
+ expect(out.length).toBe(WEBGL_LABEL_MAX_CHARS);
+ expect(out.endsWith("...")).toBe(true);
+ });
+
+ it("collectWebGLLabels follows canvas LOD bands", () => {
+ const nodes = new Float32Array([
+ 0, 0, 50, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
+ 10, 20, 25, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
+ 30, 40, 25, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
+ ]);
+ const labelByIndex = ["Local", "eth0", "Alice"];
+ const idByIndex = ["me", "eth0", "peer1"];
+ const base = { sceneCount: 3, nodes, labelByIndex, idByIndex };
+
+ expect(collectWebGLLabels({ ...base, zoom: 0.1 })).toEqual([]);
+ expect(collectWebGLLabels({ ...base, zoom: 0.3 }).map((l) => l.text)).toEqual(["Local"]);
+ expect(
+ collectWebGLLabels({ ...base, zoom: 0.3, hoverId: "peer1" }).map((l) => l.text).sort()
+ ).toEqual(["Alice", "Local"]);
+ const high = collectWebGLLabels({ ...base, zoom: 0.8 });
+ expect(high.map((l) => l.text)).toEqual(["Local", "eth0", "Alice"]);
+ expect(high[0].fontSize).toBe(16);
+ expect(high[1].fontSize).toBe(11);
+ });
+
it("graphToSceneRequest maps me/iface/peer colors and kinds", () => {
const req = graphToSceneRequest(
[


──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────